﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using static LightCAD.MathLib.Constants;
using LightCAD.Three.OpenGL;
using LightCAD.MathLib;

namespace LightCAD.Three
{
    public class WebGLUniformsGroups
    {
        private struct UniformsGroupInfo
        {
            public int boundary; // bytes
            public int storage; // bytes
        }

        public JsObj<int, int?> buffers = new JsObj<int, int?>();
        public JsObj<int, int?> updateList = new JsObj<int, int?>();
        public ListEx<int> allocatedBindingPoints = new ListEx<int>();

        private WebGLInfo info;
        private WebGLCapabilities capabilities;
        private WebGLState state;

        // binding points are global whereas block indices are per shader program
        public static readonly int maxBindingPoints = Convert.ToInt32(gl.getParameter(gl.MAX_UNIFORM_BUFFER_BINDINGS));

        public WebGLUniformsGroups(WebGLInfo info, WebGLCapabilities capabilities, WebGLState state)
        {
            this.info = info;
            this.capabilities = capabilities;
            this.state = state;
        }

        public void bind(UniformsGroup uniformsGroup, WebGLProgram program)
        {

            var webglProgram = program.program;
            state.uniformBlockBinding(uniformsGroup, webglProgram);

        }

        public void update(UniformsGroup uniformsGroup, WebGLProgram program)
        {
            var buffer = buffers[uniformsGroup.id];
            if (buffer == null)
            {
                prepareUniformsGroup(uniformsGroup);

                buffer = createBuffer(uniformsGroup);
                buffers[uniformsGroup.id] = buffer;

                uniformsGroup.addEventListener("dispose", onUniformsGroupsDispose);

            }

            // ensure to update the binding points/block indices mapping for this program

            var webglProgram = program.program;
            state.updateUBOMapping(uniformsGroup, webglProgram);

            // update UBO once per frame

            var frame = info.render.frame;

            if (updateList[uniformsGroup.id] != frame)
            {

                updateBufferData(uniformsGroup);

                updateList[uniformsGroup.id] = frame;

            }

        }

        private int createBuffer(UniformsGroup uniformsGroup)
        {

            // the setup of an UBO is independent of a particular shader program but global

            var bindingPointIndex = allocateBindingPointIndex();
            uniformsGroup.__bindingPointIndex = bindingPointIndex;

            var buffer = gl.createBuffer();
            var size = uniformsGroup.__size;
            var usage = uniformsGroup.usage;

            gl.bindBuffer(gl.UNIFORM_BUFFER, buffer);
            gl.bufferData(gl.UNIFORM_BUFFER, size, null, usage);
            gl.bindBuffer(gl.UNIFORM_BUFFER, 0);
            gl.bindBufferBase(gl.UNIFORM_BUFFER, bindingPointIndex, buffer);

            return buffer;

        }

        private int allocateBindingPointIndex()
        {

            for (int i = 0; i < maxBindingPoints; i++)
            {

                if (allocatedBindingPoints.IndexOf(i) == -1)
                {

                    allocatedBindingPoints.Push(i);
                    return i;

                }

            }

            console.error("THREE.WebGLRenderer: Maximum number of simultaneously usable uniforms groups reached.");

            return 0;
        }

        private void updateBufferData(UniformsGroup uniformsGroup)
        {

            var buffer = buffers[uniformsGroup.id];
            var uniforms = uniformsGroup.uniforms;
            var cache = uniformsGroup.__cache;

            gl.bindBuffer(gl.UNIFORM_BUFFER, buffer.Value);

            for (int i = 0, il = uniforms.Length; i < il; i++)
            {
                var uniform = uniforms[i];
                // partly update the buffer if necessary
                if (hasUniformChanged(uniform, i, cache))
                {
                    var offset = uniform.__offset;

                    Array values = (uniform.value as Array) ?? new object[] { uniform.value };
                    int arrayOffset = 0;
                    bool isArrayEle = this.isArrayEle(uniform);
                    if (isArrayEle)
                    {
                        var info = getUniformSize((uniform.value as IList)[0], true);
                        var _dataArr = uniform.__data as float[];
                        if (uniform.value is int[])
                        {
                            Span<float> span = MemoryMarshal.Cast<int, float>(uniform.value as int[]);
                            Span<float> _data = _dataArr;
                            for (int j = 0; j < span.Length; j++)
                            {
                                _data[j * 4] = span[j];
                            }
                        }
                        else if (uniform.value is float[])
                        {
                            var val = uniform.value as float[];
                            for (int j = 0; j < val.Length; j++)
                            {
                                _dataArr[j * 4] = val[j];
                            }
                        }
                        else
                        {
                            //by yf 这里用接口调用toArray性能下降5倍左右！！！！
                            if (uniform.value is Vector3[])
                            {
                                var vArr = uniform.value as Vector3[];
                                for (int j = 0; j < vArr.Length; j++)
                                {
                                    var jOff = j * 4;
                                    var vec = vArr[j];
                                    _dataArr[jOff] = (float)vec.X;
                                    _dataArr[jOff + 1] = (float)vec.Y;
                                    _dataArr[jOff + 2] = (float)vec.Z;
                                }
                            }
                            else if (uniform.value is Vector4[])
                            {
                                var vArr = uniform.value as Vector4[];
                                for (int j = 0; j < vArr.Length; j++)
                                {
                                    var jOff = j * 4;
                                    var vec = vArr[j];
                                    _dataArr[jOff] = (float)vec.X;
                                    _dataArr[jOff + 1] = (float)vec.Y;
                                    _dataArr[jOff + 2] = (float)vec.Z;
                                    _dataArr[jOff + 3] = (float)vec.W;
                                }
                            }
                            else
                            {
                                for (int j = 0; j < values.Length; j++)
                                {
                                    (values.GetValue(j) as IIOArray).ToArray(_dataArr, arrayOffset);
                                    arrayOffset += info.storage / 4;
                                }
                            }
                        }
                    }//threejs 原来的似乎对数组对象的支持有点问题
                    else
                    {
                        for (int j = 0; j < values.Length; j++)
                        {

                            var value = values.GetValue(j);

                            var info = getUniformSize(value, false);

                            if (!(uniform.value is Array))
                            {
                                if (value is int || value is float)
                                {

                                    uniform.__data[0] = value;
                                    gl.bufferSubData(gl.UNIFORM_BUFFER, offset + arrayOffset, uniform.__data.byteSize(), uniform.__data as Array);

                                }
                                else if (value is Matrix3)
                                {
                                    var mat3 = value as Matrix3;
                                    // manually converting 3x3 to 3x4

                                    uniform.__data[0] = mat3.Elements[0];
                                    uniform.__data[1] = mat3.Elements[1];
                                    uniform.__data[2] = mat3.Elements[2];
                                    uniform.__data[3] = mat3.Elements[0];
                                    uniform.__data[4] = mat3.Elements[3];
                                    uniform.__data[5] = mat3.Elements[4];
                                    uniform.__data[6] = mat3.Elements[5];
                                    uniform.__data[7] = mat3.Elements[0];
                                    uniform.__data[8] = mat3.Elements[6];
                                    uniform.__data[9] = mat3.Elements[7];
                                    uniform.__data[10] = mat3.Elements[8];
                                    uniform.__data[11] = mat3.Elements[0];

                                }
                            }
                            else
                            {
                                if (value is int || value is float)
                                    uniform.__data[arrayOffset] = value;
                                else
                                {
                                    (value as IIOArray).ToArray(uniform.__data as float[], arrayOffset);
                                }

                                arrayOffset += info.storage / 4; //info.storage / Float32Array.BYTES_PER_ELEMENT;
                            }
                        }
                    }
                    gl.bufferSubData(gl.UNIFORM_BUFFER, offset, uniform.__data.byteSize(), uniform.__data as Array);//uniform.__data as Array);
                }

            }

            gl.bindBuffer(gl.UNIFORM_BUFFER, 0);

        }

        private bool hasUniformChanged(Uniform uniform, int index, JsObj<int, object> cache)
        {
            //by yf
            if (!uniform.needsUpdate.Value)
                return false;
            if (!uniform.checkUBOCahce)
                return true;

            var value = uniform.value;

            if (cache[index] == null)
            {

                // cache entry does not exist so far

                //if ( typeof value === 'number' )
                if (value is float || value is int || value is uint)
                {
                    cache[index] = value;
                }
                else
                {
                    Array values = (value as Array) ?? new object[] { value };

                    var tempValues = new ListEx<object>();

                    for (int i = 0; i < values.Length; i++)
                    {
                        var tmpValue = values.GetValue(i);
                        if (tmpValue is float || tmpValue is int)
                            tempValues.Push(tmpValue);
                        else
                            tempValues.Push(tmpValue.InvokeMethod("clone"));
                    }

                    cache[index] = tempValues.ToArray();

                }

                return true;

            }
            else
            {

                // compare current value with cached entry

                //if (typeof value === 'number')
                if (value is int || value is uint || value is float)
                {
                    if (cache[index] != value)
                    {
                        cache[index] = value;
                        return true;
                    }
                }
                else
                {

                    Array cachedObjects = (cache[index] as Array) ?? new object[] { cache[index] };
                    var values = (value as Array) ?? new object[] { value };
                    for (int i = 0; i < cachedObjects.Length; i++)
                    {

                        var cachedObject = cachedObjects.GetValue(i);
                        var currValue = values.GetValue(i);
                        bool equal = false;
                        if (cachedObject is IIOArray)
                        {
                            equal = (cachedObject as IIOArray).Equals(currValue as IIOArray);
                            if (!equal)
                            {
                                for (int j = i; j < cachedObjects.Length; j++)
                                {
                                    cachedObjects.GetValue(j).InvokeMethod("copy", values.GetValue(j));
                                }
                            }
                        }
                        else
                        {
                            if (currValue is float)
                                equal = (float)cachedObject == (float)currValue;
                            else
                                equal = (int)cachedObject == (int)currValue;
                            if (!equal)
                            {
                                for (int j = i; j < cachedObjects.Length; j++)
                                {
                                    cachedObjects.SetValue(values.GetValue(j), j);
                                }
                            }
                        }

                        //if (cachedObject.equals(values[i]) === false)
                        if (!equal)
                        {
                            return true;
                        }

                    }

                }

            }

            return false;

        }

        private object prepareUniformsGroup(UniformsGroup uniformsGroup)
        {

            // determine total buffer size according to the STD140 layout
            // Hint: STD140 is the only supported layout in WebGL 2

            var uniforms = uniformsGroup.uniforms;

            int offset = 0; // global buffer offset in bytes
            int chunkSize = 16; // size of a chunk in bytes
            int chunkOffset = 0; // offset within a single chunk in bytes

            for (int i = 0, l = uniforms.Length; i < l; i++)
            {

                var uniform = uniforms[i];

                var infos = new UniformsGroupInfo()
                {
                    boundary = 0,
                    storage = 0
                };

                Array values = null;
                if (uniform.value is Array)
                {
                    values = uniform.value as Array;
                }
                else
                {
                    values = new object[] { uniform.value };
                }
                bool isArrayEle = this.isArrayEle(uniform);//是否是数组元素
                for (int j = 0, jl = values.Length; j < jl; j++)
                {

                    var value = values.GetValue(j);

                    var info = getUniformSize(value, isArrayEle);

                    infos.boundary += info.boundary;
                    infos.storage += info.storage;

                }

                // the following two properties will be used for partial buffer updates

                //uniform.__data = new Float32Array(infos.storage / Float32Array.BYTES_PER_ELEMENT);
                uniform.__data = new float[infos.storage / 4];
                uniform.__offset = offset;

                //by yf
                //GLSL标准Uniform Block(std140)布局规则 - fangcun的文章 - 知乎https://zhuanlan.zhihu.com/p/568323076
                //threejs 对std 140的布局规则仅仅适配了单个元素没有适配数组？？
                if (i > 0)
                {

                    chunkOffset = offset % chunkSize;

                    var remainingSizeInChunk = chunkSize - chunkOffset;

                    // check for chunk overflow

                    if (chunkOffset != 0 && (remainingSizeInChunk - infos.boundary) < 0)
                    {

                        // add padding and adjust offset

                        offset += (chunkSize - chunkOffset);
                        uniform.__offset = offset;

                    }
                }
                offset += infos.storage;
            }

            // ensure correct final padding

            chunkOffset = offset % chunkSize;

            if (chunkOffset > 0) offset += (chunkSize - chunkOffset);

            //

            uniformsGroup.__size = offset;
            uniformsGroup.__cache = new JsObj<int, object>();

            return this;

        }

        private bool isArrayEle(Uniform uniform)
        {
            return uniform.value is float[] || (uniform.value is int[] || (uniform.value is Vector2[])
                || uniform.value is Vector3[] || uniform.value is Vector4[]
                || uniform.value is Matrix3[] || uniform.value is Matrix4[]);
        }
        private UniformsGroupInfo getUniformSize(object value, bool isArrayEle)
        {

            var info = new UniformsGroupInfo
            {
                boundary = 0, // bytes
                storage = 0 // bytes
            };

            // determine sizes according to STD140
            //by yf
            if (isArrayEle && !(value is Matrix3 || value is Matrix4))
            {
                info.boundary = 16;
                info.storage = 16;
                return info;
            }
            if (value is float || value is int)
            {
                // float/int
                info.boundary = 4;
                info.storage = 4;
            }
            else if (value is Vector2)
            {

                // vec2

                info.boundary = 8;
                info.storage = 8;

            }
            else if (value is Vector3 || value is Color)
            {

                // vec3

                info.boundary = 16;
                info.storage = 12; // evil: vec3 must start on a 16-byte boundary but it only consumes 12 bytes

            }
            else if (value is Vector4)
            {

                // vec4

                info.boundary = 16;
                info.storage = 16;

            }
            else if (value is Matrix3)
            {

                // mat3 (in STD140 a 3x3 matrix is represented as 3x4)

                info.boundary = 48;
                info.storage = 48;

            }
            else if (value is Matrix4)
            {

                // mat4

                info.boundary = 64;
                info.storage = 64;

            }
            else if (value is Texture)
            {

                console.warn("THREE.WebGLRenderer: Texture samplers can not be part of an uniforms group.");

            }
            else
            {

                console.warn("THREE.WebGLRenderer: Unsupported uniform value type.", value);

            }

            return info;

        }

        private void onUniformsGroupsDispose(EventArgs _event)
        {
            var uniformsGroup = _event.target as UniformsGroup;
            uniformsGroup.removeEventListener("dispose", onUniformsGroupsDispose);
            var index = allocatedBindingPoints.IndexOf(uniformsGroup.__bindingPointIndex.Value);
            if (index >= 0)
                allocatedBindingPoints.Splice(index, 1);
            gl.deleteBuffer(this.buffers[uniformsGroup.id].Value);
            buffers[uniformsGroup.id] = null;
            updateList[uniformsGroup.id] = null;
        }

        public void dispose()
        {
            foreach (var item in buffers)
            {
                if (item.Value != null)
                    gl.deleteBuffer(item.Value.Value);
            }
            allocatedBindingPoints = new ListEx<int>();
            buffers = new JsObj<int, int?>();
            updateList = new JsObj<int, int?>();

        }
    }
}
